#!/usr/bin/env python

from tkinter import *
from tkinter import ttk
from tkinter import font
import time
import SOMUtils


elapsed = 0
frame = 0


def measureFPS(nextFrame):
    def timeIt(*args, **kwargs):
        global elapsed
        global frame
        start = time.time()
        result = nextFrame(*args, **kwargs)
        end = time.time()
        elapsed += end - start
        frame += 1
        if elapsed > 1:
            print(round(frame / elapsed, 2), 'fps')
            elapsed = 0
            frame = 0

    return timeIt



class SOMDisplay(Frame):
    def __init__(self, parent):
        Frame.__init__(self, parent)
        self.parent = parent
        self.som = SOMUtils.SelfOrgMap(SOMUtils.getRandomRGB)
        self.makeCanvas()
        self.canvas.bind('<Motion>', self.inspectNode)
        self.canvas.bind('<Leave>', self.clearNodeInfo)
        self.canvas.bind('<ButtonRelease-1>', self.pinNodeAtEvent)
        self.inspectedNode = None
        self.inspectedNodePinned = False


    def makeCanvas(self):
        self.canvas = Canvas(self, width = 510, height = 510)
        self.canvas.grid(sticky = NSEW)

    def drawNode(self, node):
        # top left corner
        x1, y1 = node.col * self.nw + 1, node.row * self.nh + 1
        # bottom right corner
        x2, y2 = x1 + self.nw, y1 + self.nh
        color = SOMUtils.getColor(node.weight)
        node.id = self.canvas.create_rectangle(x1, y1, x2, y2, fill = color, width = 0)


    def drawPin(self, node):
        # top left corner
        x1, y1 = node.col * self.nw + 1, node.row * self.nh + 1
        # bottom right corner
        x2, y2 = x1 + self.nw, y1 + self.nh
        xmargin = self.nw // 4
        ymargin = self.nh // 4
        node.pinID = self.canvas.create_oval(x1 + xmargin, y1 + ymargin, x2 - xmargin - 1, y2 - ymargin - 1, fill = "#FFFFFF", width = 1)

    def erasePin(self, node):
        self.canvas.delete(node.pinID)

    def redrawPinIfNeeded(self, node):
        if self.inspectedNode is not None:
            if self.inspectedNodePinned:
                if self.inspectedNode.id == node.id:
                    self.drawPin(node)

    def drawSOM(self, event = None):
        self.canvas.delete(ALL)
        self.nh = int(self.canvas.cget('height')) // self.som.rows   # node height
        self.nw = int(self.canvas.cget('width')) // self.som.cols    # node width
        for node in self.som:
            self.drawNode(node)


    def initSOM(self, **kwargs):
        self.som.config(**kwargs)
        self.som.initLearning()
        self.drawSOM()


    @measureFPS
    def nextIt(self):
        nextIteration = self.som.nextIteration()
        for changedNode in nextIteration:
            self.canvas.delete(changedNode.id)
            self.drawNode(changedNode)
            self.redrawPinIfNeeded(changedNode)
        self.updateInspectedNodeLabels()
        self.updateSOMStateLabels()
        if self.som.doneLearning:
            self.parent.doneLearning()


    def playPause(self):
        self.som.paused = not self.som.paused
        while not self.som.doneLearning:
            if self.som.paused:
                self.parent.pause()
                break
            self.parent.play()
            self.nextIt()
            self.canvas.update()


    def previousIt(self):
        previousIteration = self.som.previousIteration()
        for changedNode in previousIteration:
            self.canvas.delete(changedNode.id)
            self.drawNode(changedNode)
            self.redrawPinIfNeeded(changedNode)
        self.updateInspectedNodeLabels()
        self.updateSOMStateLabels()


    def getNodeAtEvent(self, event):
        col = event.x // self.nw
        row = event.y // self.nh
        return self.som.getNode(row, col)


    def inspectNode(self, event):
        if self.inspectedNodePinned:
            return
        node = self.getNodeAtEvent(event)
        self.inspectedNode = node
        self.updateInspectedNodeLabels()


    def updateInspectedNodeLabels(self):
        if self.inspectedNode is not None:
            self.parent.updateInspectedNodeLabels(self.inspectedNode)
        else:
            self.clearNodeInfo()


    def updateSOMStateLabels(self):
        self.parent.updateSOMStateLabels( self.som )


    def clearNodeInfo(self, event = None):
        if self.inspectedNodePinned:
            return
        self.inspectedNode = None
        self.parent.clearNodeInfo()


    def pinNodeAtEvent(self, event):
        node = self.getNodeAtEvent(event)
        self.pinNode(node)


    def pinNode(self, node):
        if node is None:
            self.erasePin(self.inspectedNode)
            self.inspectedNode = None
            self.inspectedNodePinned = False
        elif self.inspectedNodePinned and self.inspectedNode == node:
            self.erasePin(self.inspectedNode)
            self.inspectedNodePinned = False
        elif self.inspectedNodePinned and self.inspectNode != node:
            self.erasePin(self.inspectedNode)
            self.inspectedNode = node
            self.inspectedNodePinned = True
            self.drawPin(self.inspectedNode)
        else:
            self.inspectedNode = node
            self.inspectedNodePinned = True
            self.drawPin(self.inspectedNode)
        self.updateInspectedNodeLabels()


    def stop(self):
        self.som.doneLearning = True
        self.parent.doneLearning()



class SOMApp(Frame):
    def __init__(self, parent = None):
        Frame.__init__(self, parent)
        self.grid(sticky = NSEW)
        self.bold = font.Font(size = '-12', weight = font.BOLD)
        self.mappedToLast = None
        top = self.winfo_toplevel()
        top.resizable(width = FALSE, height = FALSE)
        top.title('SOM Explorer')
        self.reset()




    def makeControlPanel(self):
        self.controlPanel = Frame(self)
        self.makeControlVars()
        self.makeMainMenu()
        self.makeSomStatePanel()
        ttk.Separator(self.controlPanel).grid(row = 1, columnspan = 2, sticky = EW, pady = 5)
        self.makeNodeInspectionPanel()
        ttk.Separator(self.controlPanel).grid(row = 3, columnspan = 2, sticky = EW, pady = 5)
        self.makeMappingPanel()
        ttk.Separator(self.controlPanel).grid(row = 5, columnspan = 2, sticky = EW, pady = 5)
        self.makeMapControls()
        self.disableMapping()

        self.controlPanel.grid(row = 0, column = 0, rowspan = 2, sticky = NSEW)

    def makeControlVars(self):

        self.configurationParameterInfo = StringVar()

        # SOM init
        self.rows = IntVar()
        self.cols = IntVar()
        self.lrate = DoubleVar()
        self.lrateDecay = DoubleVar()
        self.nhood = DoubleVar()
        self.nhDissipation = DoubleVar()
        self.nhoodDecay = DoubleVar()
        self.maxScans = IntVar()
        self.inputSizeFactor = IntVar()

        self.resetToDefaults()

        # SOM state
        self.learning = BooleanVar()
        self.state = StringVar()
        self.stateStep = StringVar()
        self.stateScan = StringVar()
        self.stateNextInput = StringVar()
        self.stateBMU = StringVar()
        self.stateLearingRate = StringVar()
        self.neighborhoodSize = StringVar()

        # node inspection
        self.nodeCol = StringVar()
        self.nodeRow = StringVar()
        self.nodeWeight = StringVar()
        self.nodeMappings = StringVar()
        self.nodeMappingsColors = StringVar()

        # mapping
        self.inputVector = StringVar()
        self.matchedNode = StringVar()


    def parseRowsScale(self, x):
        self.rows.set(round(float(x)))

    def parseColsScale(self, x):
        self.cols.set(round(float(x)))

    def parseLrateScale(self, x):
        self.lrate.set(round(float(x), 2))

    def parseLrateDecayScale(self, x):
        self.lrateDecay.set(round(float(x), 2))

    def parseNhDissipationScale(self, x):
        self.nhDissipation.set(round(float(x), 2))

    def parseNhoodDecayScale(self, x):
        self.nhoodDecay.set(round(float(x), 2))

    def parseMaxScansDecayScale(self, x):
        self.maxScans.set(round(float(x)))

    def parseInputSizeFactorScale(self, x):
        self.inputSizeFactor.set(round(float(x)))

    def resetToDefaults(self):
        dc = self.somDisplay.som.getDefaultConfig()
        self.rows.set(dc['rows'])
        self.cols.set(dc['cols'])
        self.lrate.set(dc['lrate'])
        self.lrateDecay.set(dc['lrateDecay'])
        self.nhood.set(dc['nhood'])
        self.nhDissipation.set(dc['nhDissipation'])
        self.nhoodDecay.set(dc['nhoodDecay'])
        self.maxScans.set(dc['maxScans'])
        self.inputSizeFactor.set(dc['inputSizeFactor'])

    def showRowsParameterInfo(self, event):
        self.configurationParameterInfo.set('Rows - number of rows map has')
        self.configScaleInfoLabel.config(textvariable = self.configurationParameterInfo)
        self.configScaleInfoLabel.update_idletasks()

    def showColsParameterInfo(self, event):
        self.configurationParameterInfo.set('Cols - number of columns map has')

    def showLrateParameterInfo(self, event):
        self.configurationParameterInfo.set('Initial learning rate')

    def showLrateDecayParameterInfo(self, event):
        self.configurationParameterInfo.set('Learning rate decay - factor for decreasing lrate after each scan')

    def showRNhDissipationParameterInfo(self, event):
        self.configurationParameterInfo.set('Neighborhood dissipation - factor for adjusting how strongly are more distant neighbors of BMU affected')

    def showNhoodDecayParameterInfo(self, event):
        self.configurationParameterInfo.set('Neighborhood decay - factor for decreasing nhood after each scan')

    def showMaxScansDecayParameterInfo(self, event):
        self.configurationParameterInfo.set('Max scans - maximum numbers of passes through input data in training phase')

    def showInputSizeFactorParameterInfo(self, event):
        self.configurationParameterInfo.set('Input size factor - number of input vectors = number of nodes * inputSizeFactor')

    def cleanParameterInfo(self, event):
        self.configurationParameterInfo.set('')
        self.configScaleInfoLabel.config(textvariable = self.configurationParameterInfo)
        self.configScaleInfoLabel.update_idletasks()

    def showConfig(self):
        self.configView = Toplevel(self)
        self.configView.title('Config SOM')
        self.configView.geometry('310x340')
        self.configView.resizable(width = FALSE, height = FALSE)
        self.configView.grab_set()

        #Init panel labels
        self.initPanel = Frame(self.configView)
        Label(self.initPanel, text = 'Select initial values', font = self.bold).grid(row = 0, column = 0, sticky = W)
        Label(self.initPanel, text = 'Rows on map:')         .grid(row = 1, column = 0, sticky = W)
        Label(self.initPanel, text = 'Cols on map:')         .grid(row = 2, column = 0, sticky = W)
        Label(self.initPanel, text = 'Initial learning rate:').grid(row = 3, column = 0, sticky = W)
        Label(self.initPanel, text = 'Learning rate decay:').grid(row = 4, column = 0, sticky = W)
        Label(self.initPanel, text = 'Neighborhood dissipation:').grid(row = 5, column = 0, sticky = W)
        Label(self.initPanel, text = 'Neighborhood decay:').grid(row = 6, column = 0, sticky = W)
        Label(self.initPanel, text = 'Max scans:').grid(row = 7, column = 0, sticky = W)
        Label(self.initPanel, text = 'Input size factor:').grid(row = 8, column = 0, sticky = W)

        #Init panel scales
        rowsScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseRowsScale, from_= 1, to = 30, variable = self.rows)
        rowsScale.grid(row = 1, column = 1, sticky = NSEW)
        rowsScale.bind('<Motion>', self.showRowsParameterInfo)
        rowsScale.bind('<Leave>', self.cleanParameterInfo)

        colsScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseColsScale, from_= 1, to = 30, variable = self.cols)
        colsScale.grid(row = 2, column = 1, sticky = NSEW)
        colsScale.bind('<Motion>', self.showColsParameterInfo)
        colsScale.bind('<Leave>', self.cleanParameterInfo)

        lrateScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseLrateScale, from_= 0.01, to = 0.99, variable = self.lrate)
        lrateScale.grid(row = 3, column = 1, sticky = NSEW)
        lrateScale.bind('<Motion>', self.showLrateParameterInfo)
        lrateScale.bind('<Leave>', self.cleanParameterInfo)

        lrateDecayScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseLrateDecayScale, from_= 0.01, to = 0.99, variable = self.lrateDecay)
        lrateDecayScale.grid(row = 4, column = 1, sticky = NSEW)
        lrateDecayScale.bind('<Motion>', self.showLrateDecayParameterInfo)
        lrateDecayScale.bind('<Leave>', self.cleanParameterInfo)

        nhDissipationScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseNhDissipationScale, from_= 0.00, to = 1.00, variable = self.nhDissipation)
        nhDissipationScale.grid(row = 5, column = 1, sticky = NSEW)
        nhDissipationScale.bind('<Motion>', self.showRNhDissipationParameterInfo)
        nhDissipationScale.bind('<Leave>', self.cleanParameterInfo)

        nhoodDecayScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseNhoodDecayScale, from_= 0.01, to = 1.00, variable = self.nhoodDecay)
        nhoodDecayScale.grid(row = 6, column = 1, sticky = NSEW)
        nhoodDecayScale.bind('<Motion>', self.showNhoodDecayParameterInfo)
        nhoodDecayScale.bind('<Leave>', self.cleanParameterInfo)

        maxScansDecayScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseMaxScansDecayScale, from_= 1, to = 20, variable = self.maxScans)
        maxScansDecayScale.grid(row = 7, column = 1, sticky = NSEW)
        maxScansDecayScale.bind('<Motion>', self.showMaxScansDecayParameterInfo)
        maxScansDecayScale.bind('<Leave>', self.cleanParameterInfo)

        inputSizeFactorScale = ttk.Scale(self.initPanel, orient = 'horizontal', command = self.parseInputSizeFactorScale, from_= 1, to = 10, variable = self.inputSizeFactor)
        inputSizeFactorScale.grid(row = 8, column = 1, sticky = NSEW)
        inputSizeFactorScale.bind('<Motion>', self.showInputSizeFactorParameterInfo)
        inputSizeFactorScale.bind('<Leave>', self.cleanParameterInfo)

        #Init panel scales' values
        Label(self.initPanel, textvariable = self.rows)           .grid(row = 1, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.cols)           .grid(row = 2, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.lrate)          .grid(row = 3, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.lrateDecay)     .grid(row = 4, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.nhDissipation)  .grid(row = 5, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.nhoodDecay)     .grid(row = 6, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.maxScans)       .grid(row = 7, column = 2, sticky = W)
        Label(self.initPanel, textvariable = self.inputSizeFactor).grid(row = 8, column = 2, sticky = W)

        self.initPanel.grid(row = 0, column = 0, sticky = NSEW)
        self.initPanel.columnconfigure(0, minsize = 135)
        self.initPanel.columnconfigure(1, minsize = 135)

        # Initpanel controls
        self.initPanelControls = Frame(self.configView)
        self.initPanelControls.defaults = ttk.Button(self.initPanelControls, text = 'Defaults', command = self.resetToDefaults)
        self.initPanelControls.cancel = ttk.Button(self.initPanelControls, text = 'Cancel', command = self.configView.destroy)
        self.initPanelControls.done = ttk.Button(self.initPanelControls, text = 'Done', command = self.initLearning)

        self.initPanelControls.defaults.grid(row = 1, column = 1, sticky = S, padx = 25)
        self.initPanelControls.cancel.grid(row = 1, column = 2, sticky = S, padx = 5)
        self.initPanelControls.done.grid(row = 1, column = 3, sticky = S)

        self.initPanelControls.grid(row = 1, column = 0, sticky = EW, pady = 20)

        # Initpanel sliders info label
        self.configStatusPanel = Frame(self.configView)
        self.configScaleInfoLabel = Label(self.configStatusPanel, textvariable = self.configurationParameterInfo, font = self.bold, wraplength = "280")
        self.configScaleInfoLabel.grid(row = 1, column = 1, sticky = W)
        self.configStatusPanel.grid(row = 2, column = 0, sticky = EW)

    def showHelp(self):
        helpView = Toplevel(self)
        helpView.title('Help')
        helpView.geometry('535x630')
        helpView.resizable(width = FALSE, height = FALSE)
        helpView.grab_set()

        helpContent = ttk.Notebook(helpView, padding = "10")

        mainWindow = ttk.Frame(helpContent)
        configWindow = ttk.Frame(helpContent)

        helpContent.add(mainWindow, text='Main window')
        helpContent.add(configWindow, text='Config window')

        # MAIN WINDOW LABELS HELP TEXT
        text2 = Text(mainWindow, height = 85, width = 60, pady = 5, padx = 5)
        scroll = ttk.Scrollbar(mainWindow, command = text2.yview)
        text2.configure(yscrollcommand = scroll.set)
        text2.tag_configure('big', font=('Arial', 18, 'bold', 'italic'))
        text2.tag_configure('bold', font=('Arial', 10, 'bold'))
        text2.tag_configure('body', font=('Arial', 10))
        text2.insert(END,'Main window labels\n\n', 'big')

        text2.insert(END, '* SOM state\n', 'bold')
        body = '   - Learning (paused) - is in the state of learning (step-by-step mode)\n'
        body += '   - Learning - is in the state of learning (normal / auto mode)\n'
        body += '   - Done learning - is in the state of learning complete (ready to map the input)\n'
        text2.insert(END, body, 'body')

        text2.insert(END, '* Step ', 'bold')
        text2.insert(END, '(indicates the current iteration step)\n', 'body')

        text2.insert(END, '* Scan ', 'bold')
        text2.insert(END, ' (indicates the current scan)\n', 'body')

        text2.insert(END, '* Next input ', 'bold')
        text2.insert(END, '(indicates the weight of the next input (R, G, B); the corresponding color is displayed after the labels)\n', 'body')

        text2.insert(END, '* BMU ', 'bold')
        text2.insert(END, '(indicates the best matching unit by grid address / coordinates, for example Node(2,3) means node in row 2, column 3; the corresponding color is displayed after the labels; clicking on the label (BMU) will pin the node on the map)\n', 'body')

        text2.insert(END, '* Current learning rate ', 'bold')
        text2.insert(END, '(indicates the current learning rate value)\n', 'body')

        text2.insert(END, '* Neighborhood size ', 'bold')
        text2.insert(END, '(indicates the current neighborhood size)\n\n\n', 'body')

        text2.insert(END, '* Node inspection ', 'bold')
        text2.insert(END, '[in state Learning (paused) / Learning]\n', 'body')

        text2.insert(END, '* Row ', 'bold')
        text2.insert(END, '(indicates the observable (pinned or hovered) node row)\n', 'body')

        text2.insert(END, '* Column ', 'bold')
        text2.insert(END, '(indicates the observable (pinned or hovered) node column)\n', 'body')

        text2.insert(END, '* Weight ', 'bold')
        text2.insert(END, '(indicates the observable (pinned or hovered) node weight (R, G, B); the corres-ponding color is displayed after the labels)\n\n\n', 'body')

        text2.insert(END, '* Node inspection ', 'bold')
        text2.insert(END, '[in state Done learning]\n', 'body')

        text2.insert(END, '* Row ', 'bold')
        text2.insert(END, '(indicates the mapped-to node row)\n', 'body')

        text2.insert(END, '* Column ', 'bold')
        text2.insert(END, '(indicates the mapped-to node column)\n', 'body')

        text2.insert(END, '* Weight ', 'bold')
        text2.insert(END, '(indicates the mapped-to node weight (R, G, B); the corresponding color is displayed after the labels)\n', 'body')

        text2.insert(END, '* Mappings ', 'bold')
        text2.insert(END, '(indicates the weights (R, G, B) of the suitable input vector members; the corresponding colors are displayed after the field)\n', 'body')

        text2.insert(END, '* Map input ', 'bold')
        text2.insert(END, '(triggers the mapping action)\n', 'body')

        text2.insert(END, '* Input vector ', 'bold')
        text2.insert(END, '(indicates the input vector member weight (R, G, B); the corresponding color is displayed after the label)\n', 'body')

        text2.insert(END, '* Mapped to ', 'bold')
        text2.insert(END, '(indicates the mapped-to node address / coordinates, for example Node(2,3) means node in row 2, column 3; the corresponding color is displayed after the label)\n', 'body')

        # MAIN WINDOW BUTTONS HELP TEXT
        text2.insert(END,'\n\nMain window buttons\n\n', 'big')

        text2.insert(END, '* Play button ', 'bold')
        text2.insert(END, 'will continue in the Learning a.k.a normal mode.\n\n', 'body')

        text2.insert(END, '* Pause button ', 'bold')
        text2.insert(END, 'will change it back into the Learning (paused) a.k.a in the step-by-step mode.\n\n', 'body')

        text2.insert(END, '* Next button ', 'bold')
        text2.insert(END, 'next iteration step will be executed.\n\n', 'body')

        text2.insert(END, '* Previous button ', 'bold')
        text2.insert(END, 'previous iteration step will be executed\n', 'body')

        text2.pack(side = LEFT)
        scroll.pack(side = RIGHT, fill = Y)
        text2.configure(state = "disabled")

        # CONFIG WINDOW LABELS HELP TEXT
        text2 = Text(configWindow, height = 85, width = 60, pady = 5, padx = 5)
        scroll = ttk.Scrollbar(configWindow, command = text2.yview)
        text2.configure(yscrollcommand = scroll.set)
        text2.tag_configure('big', font=('Arial', 18, 'bold', 'italic'))
        text2.tag_configure('bold', font=('Arial', 10, 'bold'))
        text2.tag_configure('body', font=('Arial', 10))
        text2.insert(END,'Config window labels\n\n', 'big')

        text2.insert(END, '* Rows on map ', 'bold')
        text2.insert(END, '(min value:1 / max value:30)\n', 'body')

        text2.insert(END, '* Cols on map ', 'bold')
        text2.insert(END, '(min value:1 / max value:30)\n', 'body')

        text2.insert(END, '* Initial learning rate ', 'bold')
        text2.insert(END, '(min value:0.01 / max value:0.99)\n', 'body')

        text2.insert(END, '* Learning rate decay ', 'bold')
        text2.insert(END, '(min value:0.01 / max value:0.99) - factor for decreasing lrate after each scan\n', 'body')

        text2.insert(END, '* Neighborhood dissipation ', 'bold')
        text2.insert(END, '(min value:0.00 / max value:1.00)  factor for adjusting how strongly are more distant neighbours of BMU affected\n', 'body')

        text2.insert(END, '* Neighborhood decay ', 'bold')
        text2.insert(END, '(min value:0.00 / max value:1.00) - factor for decreasing nhood after each scan\n\n\n', 'body')

        text2.insert(END, '* Max scans ', 'bold')
        text2.insert(END, '(min value:1 / max value:20) - maximum number of passes through input data in training phase\n', 'body')

        text2.insert(END, '* Input size factor ', 'bold')
        text2.insert(END, '(min value:1 / max value:10) - number of input vectors = number of nodes * inputSizeFactor\n', 'body')

        # CONFIG WINDOW BUTTONS HELP TEXT
        text2.insert(END,'\nConfig window buttons\n\n', 'big')

        text2.insert(END, '* Defaults button ', 'bold')
        text2.insert(END, ' resets sliders to default values.\n\n', 'body')

        text2.insert(END, '* Cancel button', 'bold')
        text2.insert(END, ' cancels actions\n\n', 'body')

        text2.insert(END, '* Done button ', 'bold')
        text2.insert(END, 'saves selected configuration. Ready for learning.\n', 'body')

        text2.pack(side = LEFT)
        scroll.pack(side = RIGHT, fill = Y)
        text2.configure(state = "disabled")

        helpContent.pack()


    def quitPressed(self):
        quit()

    def makeMainMenu(self):
        top = self.winfo_toplevel()
        self.menuBar = Menu(top)
        top['menu'] = self.menuBar

        self.menuBar.add_command(label = 'Reset', command = self.reset)
        self.menuBar.add_command(label='Config', command = self.showConfig )
        self.menuBar.add_command(label='Help', command = self.showHelp )
        self.menuBar.add_command(label = 'Exit', command = quit )


    def makeSomStatePanel(self):
        self.statePanel = Frame(self.controlPanel)
        Label(self.statePanel, text = 'SOM state:', font = self.bold).grid(row = 0, column = 0, sticky = W)
        # SOM attribute names
        Label(self.statePanel, text = 'Step:')                 .grid(row = 1, column = 0, sticky = W)
        Label(self.statePanel, text = 'Scan:')                 .grid(row = 2, column = 0, sticky = W)
        Label(self.statePanel, text = 'Next input:')           .grid(row = 3, column = 0, sticky = W)
        self.statePanel.bmu = Label(self.statePanel, text = 'BMU:', border = 2, relief = GROOVE)
        self.statePanel.bmu.grid(row = 4, column = 0, sticky = W)
        Label(self.statePanel, text = 'Current learning rate:').grid(row = 5, column = 0, sticky = W)
        Label(self.statePanel, text = 'Neighborhood size:')    .grid(row = 6, column = 0, sticky = W)

        # SOM attribute values
        Label(self.statePanel, textvariable = self.state).grid(row = 0, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.stateStep).grid(row = 1, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.stateScan).grid(row = 2, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.stateNextInput).grid(row = 3, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.stateBMU).grid(row = 4, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.stateLearingRate).grid(row = 5, column = 1, sticky = NSEW)
        Label(self.statePanel, textvariable = self.neighborhoodSize).grid(row = 6, column = 1, sticky = NSEW)

        self.statePanel.bmu.bind('<Button-1>', lambda e: self.learning.get() and self.statePanel.bmu.config(relief = SUNKEN))
        self.statePanel.bmu.bind('<ButtonRelease-1>', self.pinCurrentBMU)

        self.statePanel.nextInputColor = Label(self.statePanel)
        self.statePanel.nextInputColor.grid(row = 3, column = 2, sticky = NSEW)

        self.statePanel.bmuColor = Label(self.statePanel)
        self.statePanel.bmuColor.grid(row = 4, column = 2, sticky = NSEW)

        self.statePanel.columnconfigure(0, minsize = 135)
        self.statePanel.columnconfigure(1, weight = 1)
        self.statePanel.columnconfigure(1, minsize = 135)
        self.statePanel.columnconfigure(2, weight = 1)
        self.statePanel.columnconfigure(2, minsize = 30)
        self.statePanel.grid(row = 0, column = 0, sticky = EW)



    def makeNodeInspectionPanel(self):
        self.inspectionPanel = Frame(self.controlPanel)
        Label(self.inspectionPanel,     text = 'Node inspection', font = self.bold).grid(row = 0, column = 0, sticky = W)
        Label(self.inspectionPanel,     text = 'Row:')                             .grid(row = 1, column = 0, sticky = W)
        Label(self.inspectionPanel,     text = 'Column:')                          .grid(row = 2, column = 0, sticky = W)
        Label(self.inspectionPanel,     text = 'Weight:')                          .grid(row = 3, column = 0, sticky = W)
        ttk.Label(self.inspectionPanel, text = 'Mappings:')                        .grid(row = 4, column = 0, sticky = NW)

        Label(self.inspectionPanel, textvariable = self.nodeRow)   .grid(row = 1, column = 1, sticky = NSEW)
        Label(self.inspectionPanel, textvariable = self.nodeCol)   .grid(row = 2, column = 1, sticky = NSEW)
        Label(self.inspectionPanel, textvariable = self.nodeWeight).grid(row = 3, column = 1, sticky = NSEW)

        self.makeNodeMappingsViewer()
        self.inspectionPanel.weightColor = Label(self.inspectionPanel)
        self.inspectionPanel.weightColor.grid(row = 3, column = 2, sticky = NSEW)

        self.inspectionPanel.columnconfigure(0, minsize = 135)
        self.inspectionPanel.columnconfigure(1, weight = 1)
        self.inspectionPanel.columnconfigure(1, minsize = 135)
        self.inspectionPanel.columnconfigure(2, weight = 1)
        self.inspectionPanel.columnconfigure(2, minsize = 30)
        self.inspectionPanel.rowconfigure(4, minsize = 140)
        self.inspectionPanel.grid(row = 2, column = 0, sticky = EW)


    def makeNodeMappingsViewer(self):
        self.inspectionPanel.mappings = Listbox(self.inspectionPanel, listvariable = self.nodeMappings, width = 15, height = 3, state = 'disabled', disabledforeground = 'black')
        self.inspectionPanel.mappings.grid(row = 4, column = 1, sticky = NSEW)
        self.inspectionPanel.mappings.bind('<Button-1>', lambda e: 'break')
        self.inspectionPanel.colors = Listbox(self.inspectionPanel, listvariable = self.nodeMappingsColors, width = 1, height = 3, activestyle = 'none', takefocus = 0)
        self.inspectionPanel.colors.grid(row = 4, column = 2, sticky = NSEW)
        self.inspectionPanel.colors.bind('<Button-1>', lambda e: 'break')
        self.inspectionPanel.colors.bind('<Motion>', lambda e: 'break')

        def scroll(*args, **kwargs):
            self.inspectionPanel.mappings.yview(*args, **kwargs)
            self.inspectionPanel.colors.yview(*args, **kwargs)

        sb = ttk.Scrollbar(self.inspectionPanel.mappings, orient = VERTICAL, command = scroll)
        sb.pack(side = RIGHT, fill = Y)


    def makeMappingPanel(self):
        self.mappingPanel = Frame(self.controlPanel)
        self.mappingPanel.map = ttk.Button(self.mappingPanel, text = 'Map input', command = self.genAndMapInput)
        self.mappingPanel.map.grid(row = 0, column = 0, sticky = W)

        Label(self.mappingPanel, text = 'Input vector:').grid(row = 1, column = 0, sticky = W)
        self.mappingPanel.mappedTo = Label(self.mappingPanel, text = 'Mapped to:', border = 2, relief = GROOVE)
        self.mappingPanel.mappedTo.grid(row = 2, column = 0, sticky = W)

        Label(self.mappingPanel, textvariable = self.inputVector).grid(row = 1, column = 1, sticky = NSEW)
        Label(self.mappingPanel, textvariable = self.matchedNode).grid(row = 2, column = 1, sticky = NSEW)

        self.mappingPanel.inputColor = Label(self.mappingPanel)
        self.mappingPanel.inputColor.grid(row = 1, column = 2, sticky = NSEW)

        self.mappingPanel.bmuColor = Label(self.mappingPanel)
        self.mappingPanel.bmuColor.grid(row = 2, column = 2, sticky = NSEW)

        self.mappingPanel.mappedTo.bind('<Button-1>', lambda e: not self.learning.get() and self.mappingPanel.mappedTo.config(relief = SUNKEN))
        self.mappingPanel.mappedTo.bind('<ButtonRelease-1>', self.pinMappedTo)

        self.mappingPanel.columnconfigure(0, minsize = 135)
        self.mappingPanel.columnconfigure(1, minsize = 135)
        self.mappingPanel.columnconfigure(2, minsize = 30)
        self.mappingPanel.grid(row = 4, column = 0, sticky = EW)


    def makeMapControls(self):
        self.mapControls = Frame(self.controlPanel)
        self.mapControls.previous = ttk.Button(self.mapControls, text = 'Previous' , command = self.somDisplay.previousIt)
        self.mapControls.play     = ttk.Button(self.mapControls, text = 'Play', command = self.somDisplay.playPause)
        self.mapControls.stop     = ttk.Button(self.mapControls, text = 'Stop' , command = self.somDisplay.stop)
        self.mapControls.next     = ttk.Button(self.mapControls, text = 'Next' , command = self.somDisplay.nextIt)

        self.mapControls.previous.grid(row = 1, column = 1, sticky = S)
        self.mapControls.play    .grid(row = 1, column = 2, sticky = S)
        self.mapControls.stop    .grid(row = 1, column = 3, sticky = S)
        self.mapControls.next    .grid(row = 1, column = 4, sticky = S)

        self.mapControls.grid(row = 6, column = 0, sticky = EW)


    def makeSomDisplay(self):
        self.somDisplay = SOMDisplay(self)
        self.somDisplay.grid(row = 0, column = 1, rowspan = 2, sticky = NSEW)


    def initLearning(self):
        config = self.getConfig()
        self.somDisplay.initSOM(**config)
        self.learning.set(True)
        self.state.set('Learning (paused)')
        self.updateSOMStateLabels(self.somDisplay.som)
        self.enableMapControls()
        self.pause()
        self.clearNodeInfo()
        self.clearMappingInfo()
        try:
            self.configView.destroy()
        except AttributeError:
            pass


    def updateSOMStateLabels(self, som):
        self.stateStep.set(som.iteration)
        self.stateScan.set(som.scan)
        self.stateNextInput.set(SOMUtils.getVectorAsStr(som.nextInput))
        self.stateLearingRate.set(round(som.lrate, 10))
        self.neighborhoodSize.set(round(som.nhood, 10))
        self.stateBMU.set(som.BMU)
        self.statePanel.bmuColor.config(bg = SOMUtils.getColor(som.BMU.weight))
        self.statePanel.nextInputColor.config(bg = SOMUtils.getColor(som.nextInput))


    def getConfig(self):
        return {'rows': self.rows.get(),
                'cols': self.cols.get(),
                'lrate': self.lrate.get(),
                'lrateDecay': self.lrateDecay.get(),
                'nhood': (max(self.cols.get(), self.rows.get()) / 4),
                'nhDissipation': self.nhDissipation.get(),
                'nhoodDecay': self.nhoodDecay.get(),
                'maxScans': self.maxScans.get(),
                'inputSizeFactor': self.inputSizeFactor.get()}


    def updateInspectedNodeLabels(self, node):
        self.nodeRow.set(node.row)
        self.nodeCol.set(node.col)
        self.nodeWeight.set(node.getWeightAsStr())
        self.inspectionPanel.weightColor.config(bg = SOMUtils.getColor(node.weight))
        self.nodeMappings.set(node.getMappings())
        self.nodeMappingsColors.set(tuple(['' for x in node.mappings]))
        for i in range(len(node.mappings)):
            self.inspectionPanel.colors.itemconfig(i, bg = SOMUtils.getColor(node.mappings[i]))



    def clearNodeInfo(self):
        self.nodeRow.set('')
        self.nodeCol.set('')
        self.nodeWeight.set('')
        self.inspectionPanel.weightColor.config(bg = 'SystemButtonFace')
        self.nodeMappings.set('')
        self.nodeMappingsColors.set('')


    def clearMappingInfo(self):
        self.inputVector.set('')
        self.matchedNode.set('')
        self.mappingPanel.bmuColor.config(bg = 'SystemButtonFace')
        self.mappingPanel.inputColor.config(bg = 'SystemButtonFace')


    def pause(self):
        self.state.set('Learning (paused)')
        self.mapControls.play.config(text = 'Play')


    def play(self):
        self.state.set('Learning')
        self.mapControls.play.config(text = 'Pause')


    def doneLearning(self):
        self.learning.set(False)
        self.state.set('Done learning')
        self.stateNextInput.set('')
        self.stateBMU.set('')
        self.statePanel.bmuColor.config(bg = 'SystemButtonFace')
        self.statePanel.nextInputColor.config(bg = 'SystemButtonFace')
        self.disableMapControls()
        self.enableMapping()


    def disableMapControls(self):
        for bName in ['previous', 'play', 'stop', 'next']:
            button = getattr(self.mapControls, bName)
            button.config(state = 'disabled')


    def enableMapControls(self):
        for bName in ['previous', 'play', 'stop', 'next']:
            button = getattr(self.mapControls, bName)
            button.config(state = 'enabled')


    def disableMapping(self):
        for bName in ['map']:
            button = getattr(self.mappingPanel, bName)
            button.config(state = 'disabled')


    def enableMapping(self):
        for bName in ['map']:
            button = getattr(self.mappingPanel, bName)
            button.config(state = 'enabled')


    def pinCurrentBMU(self, event):
        if not self.learning.get():
            return
        self.statePanel.bmu.config(relief = GROOVE)
        if self.somDisplay.som.BMU == self.somDisplay.inspectedNode:
            return
        self.somDisplay.pinNode(self.somDisplay.som.BMU)


    def pinMappedTo(self, event):
        if self.learning.get():
            return
        self.mappingPanel.mappedTo.config(relief = GROOVE)
        if self.mappedToLast == None or self.mappedToLast == self.somDisplay.inspectedNode:
            return
        self.somDisplay.pinNode(self.mappedToLast)


    def reset(self):
        try:
            self.somDisplay.stop()
            self.somDisplay.destroy()
            self.controlPanel.destroy()
            self.configView.destroy()
        except AttributeError:
            pass
        self.makeSomDisplay()
        self.makeControlPanel()
        self.initLearning()


    def genAndMapInput(self):
        inputv = self.somDisplay.som.randWeightFunc()
        bmu = self.somDisplay.som.mapInput(inputv)
        self.mappedToLast = bmu
        self.mappingPanel.inputColor.config(bg = SOMUtils.getColor(inputv))
        self.mappingPanel.bmuColor.config(bg = SOMUtils.getColor(bmu.weight))
        self.somDisplay.updateInspectedNodeLabels()
        self.inputVector.set(SOMUtils.getVectorAsStr(inputv))
        self.matchedNode.set(repr(bmu))
        if self.somDisplay.inspectedNode != bmu:
            self.somDisplay.pinNode(bmu)


if __name__ == '__main__':
    SOMApp().mainloop()
